package com.psytech.app.task.service.implementation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.PersistenceException;
import javax.persistence.Query;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.springframework.stereotype.Repository;

import com.psytech.app.task.entity.implementation.Comment;
import com.psytech.app.task.entity.implementation.STMEntity;
import com.psytech.app.task.entity.implementation.Task;
import com.psytech.app.task.entity.implementation.User;

/**
 * Couche d'accés au référentiel de données.
 * 
 * @author psychoben@gmail.com
 */
@Repository
public class Dao {

	private static final Logger LOG = Logger.getLogger(Dao.class);

	private transient String sqlVersionRequest = null;

	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
	private transient EntityManager entityManager4TASK;// NOPMD

	/**
	 * Sauve le bean passé en paramètre. Si la méthode getId n'existe pas ou n'est pas publique, une RuntimeException est lancée.
	 * 
	 * @param object
	 *            l'object à persister en base
	 * @throws PersistenceException
	 *             dans le cas d'un probléme d'enregistrement en BDD, ou bien si la methode getId de l'objet n'est pas trouvée.
	 */
	public void save(final STMEntity object) throws PersistenceException {
		Object id = null;// NOPMD
		try {
			LOG.debug("sauvegarde d'une instance de " + object.getClass());
			final Method getId = object.getClass().getMethod("getId", (Class[]) null);
			id = getId.invoke(object);
			LOG.debug("Id = " + id);
		} catch (final Throwable throwable) {
			throw new PersistenceException("La m�thode getId n'existe pas ou est priv�e", throwable);
		}

		try {
			if (id != null)
				this.save(object, Boolean.FALSE);
			else
				this.save(object, Boolean.TRUE);
		} catch (final Throwable exception) {
			LOG.error("Erreur lors de l'enregistrement du bean", exception);

			throw new PersistenceException("Impossible d'enregistrer le bean", exception);
		}
	}

	private void save(final STMEntity object, final boolean persist) {
		if (persist) {
			entityManager4TASK.persist(object);
			entityManager4TASK.flush();
			entityManager4TASK.refresh(object);
		} else
			entityManager4TASK.merge(object);
	}

	/**
	 * Persiste un nouvel entity bean en base. La référence de l'objet est modifiée.
	 * 
	 * @param entity
	 *            l'objet à sauver
	 */
	public void create(final STMEntity entity) {
		entityManager4TASK.persist(entity);
	}

	/**
	 * Met à jour un entity bean dans le référentiel.
	 * 
	 * @param entity
	 *            l'object à mettre à jour
	 * @return l'objet mis à jour
	 */
	public Object update(final STMEntity entity) {
		LOG.debug("mise à jour d'un objet");
		return entityManager4TASK.merge(entity);
	}

	/**
	 * Permet de supprimer la référence à l'entité du référentiel. Une fois supprimé, l'objet ne pourra être récupéré.
	 * 
	 * @param entity
	 *            l'objet à supprimer définitivement du référentiel.
	 */
	public void remove(final STMEntity entity) {
		Object id = null;
		try {
			LOG.debug("suppression d'une instance de l'object : " + entity.getClass());
			final Method getId = entity.getClass().getMethod("getId", (Class[]) null);
			id = getId.invoke(entity);
		} catch (final Throwable throwable) {
			throw new HibernateException("La méthode getId n'existe pas ou est privée", throwable);
		}
		try {
			if (id != null) {
				final Object embeddedObjet = entityManager4TASK.find(entity.getClass(), id);
				if (embeddedObjet != null)
					entityManager4TASK.remove(embeddedObjet);
			}
		} catch (final Throwable exception) {
			LOG.error("Erreur lors de la suppression du bean", exception);
			try {
			} catch (final Throwable e) {
				LOG.error("Rollback impossible", e);
			}
			throw new HibernateException("Impossible de supprimer le bean");
		}
	}

	/**
	 * Retrouve un objet dans le référentiel à partir de son Id, ou de son UID.
	 * 
	 * @param entity
	 *            l'objet à retrouver dans le référentiel
	 * @return l'objet complété avec les données du référentiel, ou null sinon.
	 */
	public Object find(final STMEntity entity) {
		try {
			final Method getId = entity.getClass().getMethod("getId", (Class[]) null);
			if (getId.invoke(entity) == null)
				return findByUid(entity);
			return entityManager4TASK.find(entity.getClass(), getId.invoke(entity));
		} catch (final Throwable throwable) {
			LOG.error("impossible d'executer le find", throwable);
			return null;
		}
	}

	private Object findByUid(final STMEntity entity) {
		Method getUid;
		try {
			getUid = entity.getClass().getMethod("getUid", (Class[]) null);
		} catch (final Exception e) {
			throw new IllegalArgumentException("Cet objet n'a pas la m�thode getUid", e);
		}
		final Object uid;
		try {
			if (getUid.invoke(entity) == null)
				throw new IllegalStateException("uid de l'objet n'est pas d�fini, impossible d'�x�cuter la requ�te");
			else
				uid = getUid.invoke(entity);
		} catch (final IllegalArgumentException exception) {
			throw new IllegalStateException("Impossible de r�cup�r� la propri�t� uid");
		} catch (final IllegalAccessException exception) {
			throw new IllegalStateException("Impossible de r�cup�r� la propri�t� uid, droits insufisants");
		} catch (final InvocationTargetException exception) {
			throw new IllegalStateException("Probléme de réfexion");
		}
		final String query = "SELECT o from " + entity.getClass().getName() + " o where uid = ? ";
		LOG.debug("findByUid querry : " + query);
		try {
			return entityManager4TASK.createQuery(query).setParameter(1, uid).getSingleResult();
		} catch (final NoResultException exception) {
			return null;
		}

	}

	/**
	 * Renvoi la version de la base de données utilisée comme référentiel principal.
	 * 
	 * @return un String représentant le version.
	 */
	public String getDBVersion() {
		return (String) entityManager4TASK.createNativeQuery(sqlVersionRequest).getSingleResult();
	}

	/**
	 * Définie la requête à utiliser pour récupérer la version de la base de donnée. Voir aussi {@link Dao#getDBVersion}.
	 * 
	 * @param sqlVersionRequest
	 *            la requ�te au format SQL. ! Implementation dependent !
	 */
	public void setSqlVersionRequest(final String sqlVersionRequest) {
		this.sqlVersionRequest = sqlVersionRequest;
	}

	/**
	 * Renvoi la liste de toutes les tâches présentes dans le référentiel.
	 */
	@SuppressWarnings("unchecked")
	public List<Task> getAllTask() {
		return entityManager4TASK.createQuery("SELECT t FROM Task AS t ORDER BY t.priority DESC").getResultList();
	}

	/**
	 * Renvoi la liste des tâches affectées à un utilisateur.
	 * 
	 * @param user
	 *            {@link com.psytech.app.task.entity.implementation.User} pour lequel récupérer les tâches.
	 */
	@SuppressWarnings("unchecked")
	public List<Task> getAffectedTasks(final User user) {
		return entityManager4TASK.createQuery("select t from Task as t join t.users u where u.id = ? order by t.priority DESC").setParameter(1, user.getId()).getResultList();
	}

	/**
	 * Renvoi une liste d'utilisateur correspondant aux crit�res en entr�e. Par exemple, pour que la recheche se fasse sur le nom et le pr�nom, il suffit
	 * d'instancier un nouvel entity bean {@link com.psytech.app.task.entity.implementation.User} :<br/>
	 * <br/>
	 * <code>User searchCriteria = new User();</code><br/>
	 * <br/>
	 * Et de d�finir les attributs sur lesquels la recherche va porter :<br/>
	 * <br/>
	 * <code>searchCriteria.setFirstname("Linus");</code><br/>
	 * <code>searchCriteria.setLastname("Torvald");</code><br/>
	 * <br/>
	 * Toutefois les attributs de type {@link java.util.Collection} ne pourrons pas être utilisé comme crit�re de recherche et seront ignorés.
	 * 
	 * @param searchCriteria
	 *            l'entity bean {@link com.psytech.app.task.entity.implementation.User} contenant les critéres de recherche.
	 */
	@SuppressWarnings("unchecked")
	public List<User> findUsers(final User searchCriteria) {
		LOG.debug("Recherche d'un utilisateur");
		final Query query = getSimpleSelectQuery(searchCriteria, User.class);
		final List<User> users = query.getResultList();
		LOG.debug("Nombre d'objet en retour : " + users.size());
		return users;
	}

	/**
	 * Renvoi une liste de tâches correspondant aux critéres en entrée.
	 * 
	 * @see Dao#findUsers(User)
	 * @param searchCriteria
	 *            l'entity bean {@link com.psytech.app.task.entity.implementation.Task} contenant les critéres de recherche.
	 */
	@SuppressWarnings("unchecked")
	public List<Task> findTasks(final Task searchCriteria) {
		LOG.debug("Recherche d'une tâche");
		final Query query = getSimpleSelectQuery(searchCriteria, Task.class);
		final List<Task> tasks = query.getResultList();
		LOG.debug("Nombre d'objet en retour : " + tasks.size());
		return tasks;
	}

	/**
	 * Génére une {@link javax.persistance.Query} en fonction de critére de recherche. Elle est générée par rapport à l'objet passé en paramètre.<br/>
	 * Les attributs de type collection ne peuvent généré des restrictions.
	 * 
	 * @param searchCriteria
	 *            l'entityBean contenant les crit�res de recherche.
	 * @param entityBeanClass
	 *            La classe de l'entity bean a traiter.
	 */
	private Query getSimpleSelectQuery(final Object searchCriteria, final Class entityBeanClass) {
		// TODO improve pouvoir forcer à charger les collection (fetch join)
		final StringBuffer queryString = new StringBuffer();
		final Map<String, Object> parameters = new HashMap<String, Object>();
		try {
			final Map properties = BeanUtils.describe(searchCriteria);
			for (final Object key : properties.keySet())
				if (!key.toString().equals("class") && (properties.get(key) != null) && !properties.get(key).equals("[]") && !(properties.get(key) instanceof Collection))
					try {
						LOG.debug(key.toString() + " " + properties.get(key));
						Object value = PropertyUtils.getProperty(searchCriteria, key.toString());
						if (value instanceof Collection)
							LOG.debug(key.toString() + " " + properties.get(key) + " rejeté !");
						else {
							if (queryString.length() == 0)
								queryString.append(" WHERE ");
							else
								queryString.append(" AND ");
							queryString.append("o." + key.toString() + "=:" + key.toString());

							if (value instanceof STMEntity) {
								LOG.debug("STMEntity : " + value.getClass().getName());
								value = find((STMEntity) value);
								LOG.debug("Id de l'entity = " + ((STMEntity) value).getId());
							}
							parameters.put(key.toString(), value);
						}
					} catch (final Exception e) {
						LOG.info("Un des paramètres a provoqué une erreur. Le paramètre est ignoré.", e);
					}
		} catch (final Exception e) {
			LOG.error("Impossible de récupérer les critéres de la requête", e);
			throw new IllegalStateException("Impossible de récupérer les critéres de la requête");
		}
		final String className = entityBeanClass.getName().indexOf('.') > 0 ? entityBeanClass.getName().substring(entityBeanClass.getName().lastIndexOf('.') + 1) : entityBeanClass
				.getName();
		queryString.insert(0, "SELECT o from " + className + " o");
		LOG.debug("Requete générée: " + queryString.toString());

		final Query query = entityManager4TASK.createQuery(queryString.toString());
		for (final String key : parameters.keySet())
			query.setParameter(key, parameters.get(key));
		return query;
	}

	/**
	 * Renvoi la liste des commentaires, {@link Comment}, associés à une tâche, {@link Task}.
	 * 
	 * @param task
	 *            la tâche pour laquelle r�cup�rer les commentaires ( {@link Comment}).
	 * @return une liste vide dans le cas ou aucun resultat n'est trouvé.
	 */
	@SuppressWarnings("unchecked")
	public List<Comment> getComments(final Task task) {
		if (LOG.isDebugEnabled())
			LOG.debug("Récupération de tous les commentaires pour la tâche " + task.getUid());
		return entityManager4TASK.createQuery("SELECT c FROM Comment c where c.task = ? order by c.id DESC").setParameter(1, task).getResultList();
	}

	/**
	 * @param uid
	 * @return le user avec l'uid correspondant
	 */
	public User findUserByUid(final String uid) {
		final User user = new User();
		user.setUid(uid);
		return (User) findByUid(user);
	}

	/**
	 * @param uid
	 * @return la tâche avec l'uid correspondant
	 */
	public Task findTaskByUid(final String uid) {
		final Task task = new Task();
		task.setUid(uid);
		return (Task) findByUid(task);
	}

	public Comment findCommentByUid(final String uid) {
		final Comment comment = new Comment();
		comment.setUid(uid);
		return (Comment) findByUid(comment);
	}

	public List<Task> getUnaffectedTask() {
		final Query query = this.entityManager4TASK.createQuery("SELECT t from Task t where t.users IS EMPTY");
		return query.getResultList();

	}
}
